Micron Document
🎖️GitЯра🎖️

Commit 4607c3f223b07c78d3b7e58ea46f4bb9d67b04c9


Parents : 305ff23
Author : James Rich <james.a.rich@gmail.com>
Date : 2026-05-18T13:09:02-05:00

feat(map): inline map polish & location permission feedback

- InlineMap: add short name SymbolLayer label above marker
- InlineMap: adaptive zoom (zoom out for imprecise positions >500m)
- MapScreen: show snackbar when tapping location button without
permission instead of silently doing nothing
- Add map_location_unavailable string resource

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Changes
Diff

diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt
index 482263a259..61da77ce06 100644
--- a/.skills/compose-ui/strings-index.txt
+++ b/.skills/compose-ui/strings-index.txt
@@ -689,6 +689,7 @@ map_empty_state
map_filter
map_layer_formats
map_load_error
+map_location_unavailable
map_node_popup_details
map_offline_manager
map_purge_fail

diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml
index 641530db58..2ad87a94bb 100644
--- a/core/resources/src/commonMain/composeResources/values/strings.xml
+++ b/core/resources/src/commonMain/composeResources/values/strings.xml
@@ -719,6 +719,7 @@
<string name="map_filter">Map Filter\n</string>
<string name="map_layer_formats">Map layers support .kml, .kmz, or GeoJSON formats.</string>
<string name="map_load_error">Map failed to load</string>
+ <string name="map_location_unavailable">Location permission required for tracking</string>
<string name="map_node_popup_details">%1$s&lt;br&gt;Last heard: %2$s&lt;br&gt;Last position: %3$s&lt;br&gt;Battery: %4$s</string>
<string name="map_offline_manager">Offline Manager</string>
<string name="map_purge_fail">SQL Cache purge failed, see logcat for details</string>

diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt
index def0b1cc00..a8a20eabcc 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt
@@ -51,6 +51,7 @@ import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.map
import org.meshtastic.core.resources.map_empty_state
import org.meshtastic.core.resources.map_load_error
+import org.meshtastic.core.resources.map_location_unavailable
import org.meshtastic.core.resources.waypoint_deleted
import org.meshtastic.core.resources.waypoint_sent
import org.meshtastic.core.ui.component.MainAppBar
@@ -112,6 +113,7 @@ fun MapScreen(
// Snackbar messages for map load error
val mapLoadErrorMsg = stringResource(Res.string.map_load_error)
+ val locationUnavailableMsg = stringResource(Res.string.map_location_unavailable)
val waypointSentMsg = stringResource(Res.string.waypoint_sent)
val waypointDeletedMsg = stringResource(Res.string.waypoint_deleted)
@@ -263,7 +265,9 @@ fun MapScreen(
isLocationTrackingEnabled = isLocationTrackingEnabled,
isTrackingBearing = bearingUpdate == BearingUpdate.TRACK_LOCATION,
onToggleLocationTracking = {
- if (!isLocationTrackingEnabled) {
+ if (!locationAvailable) {
+ scope.launch { snackbarHostState.showSnackbar(locationUnavailableMsg) }
+ } else if (!isLocationTrackingEnabled) {
// Off → Track with bearing
bearingUpdate = BearingUpdate.TRACK_LOCATION
isLocationTrackingEnabled = true

diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/InlineMap.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/InlineMap.kt
index 8f555895d1..922af53089 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/InlineMap.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/InlineMap.kt
@@ -22,10 +22,14 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.em
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.expressions.dsl.const
+import org.maplibre.compose.expressions.dsl.offset
+import org.maplibre.compose.expressions.value.SymbolAnchor
import org.maplibre.compose.layers.CircleLayer
+import org.maplibre.compose.layers.SymbolLayer
import org.maplibre.compose.map.GestureOptions
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap
@@ -44,7 +48,10 @@ import org.meshtastic.feature.map.util.precisionBitsToMeters
import org.meshtastic.feature.map.util.toGeoPositionOrNull
private const val DEFAULT_ZOOM = 15.0
+private const val LOW_PRECISION_ZOOM = 12.0
+private const val PRECISION_THRESHOLD_METERS = 500
private const val PRECISION_CIRCLE_FILL_ALPHA = 0.15f
+private const val LABEL_OFFSET = -2f
/**
* A compact, non-interactive map showing a single node's position. Used in node detail screens. Replaces both the
@@ -55,8 +62,12 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) {
val position = node.validPosition ?: return
val geoPos = toGeoPositionOrNull(position.latitude_i, position.longitude_i) ?: return
+ // Adaptive zoom: zoom out for imprecise positions so the precision circle is visible
+ val precisionMeters = precisionBitsToMeters(position.precision_bits)
+ val zoom = if (precisionMeters > PRECISION_THRESHOLD_METERS) LOW_PRECISION_ZOOM else DEFAULT_ZOOM
+
key(node.num) {
- val cameraState = rememberCameraState(firstPosition = CameraPosition(target = geoPos, zoom = DEFAULT_ZOOM))
+ val cameraState = rememberCameraState(firstPosition = CameraPosition(target = geoPos, zoom = zoom))
val nodeFeature =
remember(node.num, geoPos) {
@@ -82,8 +93,21 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) {
strokeColor = const(Color.White),
)
+ // Short name label above the marker
+ val shortName = node.user.short_name
+ if (!shortName.isNullOrBlank()) {
+ SymbolLayer(
+ id = "inline-node-label",
+ source = source,
+ textField = const(shortName).cast(),
+ textSize = const(0.9f.em),
+ textOffset = offset(0f.em, LABEL_OFFSET.em),
+ textAnchor = const(SymbolAnchor.Bottom),
+ textColor = const(Color.DarkGray),
+ )
+ }
+
// Precision circle — radius computed from precision_meters using latitude-aware metersPerDp
- val precisionMeters = precisionBitsToMeters(position.precision_bits)
val metersPerDp = cameraState.metersPerDpAtTarget
if (precisionMeters > 0 && metersPerDp > 0) {
val radiusDp = (precisionMeters / metersPerDp).dp

Served by rngit 1.5.0 - Generated in 0.09s